/*
 * Home.java 15 mai 2006
 *
 * Sweet Home 3D, Copyright (c) 2006 Emmanuel PUYBARET / eTeks <info@eteks.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
package com.eteks.sweethome3d.model;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * The home managed by the application with its furniture and walls.
 * @author Emmanuel Puybaret
 */
public class Home implements Serializable, Cloneable
{
	private static final long serialVersionUID = 1L;
	
	/**
	 * The current version of this home. Each time the field list is changed
	 * in <code>Home</code> class or in one of the classes that it uses,
	 * this number is increased.
	 */
	public static final long CURRENT_VERSION = 5200;
	
	private static final boolean KEEP_BACKWARD_COMPATIBLITY = true;
	
	private static final Comparator<Level> LEVEL_ELEVATION_COMPARATOR = new Comparator<Level>()
	{
		public int compare(Level level1, Level level2)
		{
			int elevationComparison = Float.compare(level1.getElevation(), level2.getElevation());
			if (elevationComparison != 0)
			{
				return elevationComparison;
			}
			else
			{
				return level1.getElevationIndex() - level2.getElevationIndex();
			}
		}
	};
	
	/**
	 * The properties of a home that may change. <code>PropertyChangeListener</code>s added 
	 * to a home will be notified under a property name equal to the name value of one these properties.
	 */
	public enum Property
	{
		NAME, MODIFIED, FURNITURE_SORTED_PROPERTY, FURNITURE_DESCENDING_SORTED, FURNITURE_VISIBLE_PROPERTIES, BACKGROUND_IMAGE, CAMERA, PRINT, BASE_PLAN_LOCKED, STORED_CAMERAS, RECOVERED, REPAIRED, SELECTED_LEVEL, ALL_LEVELS_SELECTION
	};
	
	private List<HomePieceOfFurniture> furniture;
	private transient CollectionChangeSupport<HomePieceOfFurniture> furnitureChangeSupport;
	private transient List<Selectable> selectedItems;
	private transient List<SelectionListener> selectionListeners;
	private transient boolean allLevelsSelection;
	private List<Level> levels;
	private Level selectedLevel;
	private transient CollectionChangeSupport<Level> levelsChangeSupport;
	private List<Wall> walls;
	private transient CollectionChangeSupport<Wall> wallsChangeSupport;
	private List<Room> rooms;
	private transient CollectionChangeSupport<Room> roomsChangeSupport;
	private List<Polyline> polylines;
	private transient CollectionChangeSupport<Polyline> polylinesChangeSupport;
	private List<DimensionLine> dimensionLines;
	private transient CollectionChangeSupport<DimensionLine> dimensionLinesChangeSupport;
	private List<Label> labels;
	private transient CollectionChangeSupport<Label> labelsChangeSupport;
	private Camera camera;
	private String name;
	private final float wallHeight;
	private transient boolean modified;
	private transient boolean recovered;
	private transient boolean repaired;
	private BackgroundImage backgroundImage;
	private ObserverCamera observerCamera;
	private Camera topCamera;
	private List<Camera> storedCameras;
	private HomeEnvironment environment;
	private HomePrint print;
	private String furnitureSortedPropertyName;
	private List<String> furnitureVisiblePropertyNames;
	private boolean furnitureDescendingSorted;
	private Map<String, Object> visualProperties;
	private Map<String, String> properties;
	private transient PropertyChangeSupport propertyChangeSupport;
	private long version;
	private boolean basePlanLocked;
	private Compass compass;
	// The 5 following environment fields are still declared for compatibility reasons
	private int skyColor;
	private int groundColor;
	private HomeTexture groundTexture;
	private int lightColor;
	private float wallsAlpha;
	// The two following fields aren't transient for backward compatibility reasons 
	private HomePieceOfFurniture.SortableProperty furnitureSortedProperty;
	private List<HomePieceOfFurniture.SortableProperty> furnitureVisibleProperties;
	// The following field is a temporary copy of furniture containing HomeDoorOrWindow instances
	// created at serialization time for backward compatibility reasons
	private List<HomePieceOfFurniture> furnitureWithDoorsAndWindows;
	// The following field is a temporary copy of furniture containing HomeFurnitureGroup instances
	// created at serialization time for backward compatibility reasons
	private List<HomePieceOfFurniture> furnitureWithGroups;
	
	/**
	 * Creates a home with no furniture, no walls, 
	 * and a height equal to 250 cm.
	 */
	public Home()
	{
		this(250);
	}
	
	/**
	 * Creates a home with no furniture and no walls.
	 */
	public Home(float wallHeight)
	{
		this(new ArrayList<HomePieceOfFurniture>(), wallHeight);
	}
	
	/**
	 * Creates a home with the given <code>furniture</code>, 
	 * no walls and a height equal to 250 cm.
	 */
	public Home(List<HomePieceOfFurniture> furniture)
	{
		this(furniture, 250);
	}
	
	private Home(List<HomePieceOfFurniture> furniture, float wallHeight)
	{
		this.furniture = new ArrayList<HomePieceOfFurniture>(furniture);
		this.walls = new ArrayList<Wall>();
		this.wallHeight = wallHeight;
		this.furnitureVisibleProperties = Arrays
				.asList(new HomePieceOfFurniture.SortableProperty[] { HomePieceOfFurniture.SortableProperty.NAME,
						HomePieceOfFurniture.SortableProperty.WIDTH, HomePieceOfFurniture.SortableProperty.DEPTH,
						HomePieceOfFurniture.SortableProperty.HEIGHT, HomePieceOfFurniture.SortableProperty.VISIBLE });
		// Init transient lists and other fields
		init(true);
		addModelListeners();
	}
	
	/**
	 * Creates a home from an other one. All mutable data of the source <code>home</code>
	 * is cloned to this home and listeners support is reset.
	 * @since 5.0
	 */
	protected Home(Home home)
	{
		this.wallHeight = home.getWallHeight();
		copyHomeData(home, this);
		initListenersSupport(this);
		addModelListeners();
	}
	
	/**
	 * Initializes new and transient home fields to their default values 
	 * and reads home from <code>in</code> stream with default reading method.
	 */
	private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException
	{
		init(false);
		in.defaultReadObject();
		
		if (KEEP_BACKWARD_COMPATIBLITY)
		{
			// Restore furnitureSortedProperty from furnitureSortedPropertyName
			if (this.furnitureSortedPropertyName != null)
			{
				try
				{
					this.furnitureSortedProperty = HomePieceOfFurniture.SortableProperty
							.valueOf(this.furnitureSortedPropertyName);
				}
				catch (IllegalArgumentException ex)
				{
					// Ignore malformed enum constant 
				}
				this.furnitureSortedPropertyName = null;
			}
			// Restore furnitureVisibleProperties from furnitureVisiblePropertyNames
			if (this.furnitureVisiblePropertyNames != null)
			{
				this.furnitureVisibleProperties = new ArrayList<HomePieceOfFurniture.SortableProperty>();
				for (String furnitureVisiblePropertyName : this.furnitureVisiblePropertyNames)
				{
					try
					{
						this.furnitureVisibleProperties
								.add(HomePieceOfFurniture.SortableProperty.valueOf(furnitureVisiblePropertyName));
					}
					catch (IllegalArgumentException ex)
					{
						// Ignore malformed enum constants 
					}
				}
				this.furnitureVisiblePropertyNames = null;
			}
			
			// Ensure all wall have an height
			for (Wall wall : this.walls)
			{
				if (wall.getHeight() == null)
				{
					wall.setHeight(this.wallHeight);
				}
			}
			
			// Restore referenced HomeDoorOrWindow instances stored in a separate field 
			// for backward compatibility reasons
			if (this.furnitureWithDoorsAndWindows != null)
			{
				this.furniture = this.furnitureWithDoorsAndWindows;
				this.furnitureWithDoorsAndWindows = null;
			}
			
			// Restore referenced HomeFurnitureGroup instances stored in a separate field 
			// for backward compatibility reasons
			if (this.furnitureWithGroups != null)
			{
				this.furniture = this.furnitureWithGroups;
				this.furnitureWithGroups = null;
			}
			
			// Restore environment fields from home fields for compatibility reasons
			this.environment.setGroundColor(this.groundColor);
			this.environment.setGroundTexture(this.groundTexture);
			this.environment.setSkyColor(this.skyColor);
			this.environment.setLightColor(this.lightColor);
			this.environment.setWallsAlpha(this.wallsAlpha);
			
			if (this.version <= 3400)
			{
				// Automatically adjust ground color to a darker color
				int groundColor = this.environment.getGroundColor();
				this.environment.setGroundColor(((((groundColor >> 16) & 0xFF) * 3 / 4) << 16)
						| ((((groundColor >> 8) & 0xFF) * 3 / 4) << 8) | ((groundColor & 0xFF) * 3 / 4));
			}
			
			// Assign level elevation index from current order if index is equal to -1
			if (this.levels.size() > 0)
			{
				Level previousLevel = this.levels.get(0);
				if (previousLevel.getElevationIndex() == -1)
				{
					previousLevel.setElevationIndex(0);
				}
				for (int i = 1; i < this.levels.size(); i++)
				{
					Level level = this.levels.get(i);
					if (level.getElevationIndex() == -1)
					{
						if (previousLevel.getElevation() == level.getElevation())
						{
							level.setElevationIndex(previousLevel.getElevationIndex() + 1);
						}
						else
						{
							level.setElevationIndex(0);
						}
					}
					previousLevel = level;
				}
			}
			
			// Move known visual properties to string properties
			moveVisualProperty("com.eteks.sweethome3d.swing.PhotoPanel.PhotoDialogX");
			moveVisualProperty("com.eteks.sweethome3d.swing.PhotoPanel.PhotoDialogY");
			moveVisualProperty("com.eteks.sweethome3d.swing.PhotosPanel.PhotoDialogX");
			moveVisualProperty("com.eteks.sweethome3d.swing.PhotosPanel.PhotoDialogY");
			moveVisualProperty("com.eteks.sweethome3d.swing.VideoPanel.VideoDialogX");
			moveVisualProperty("com.eteks.sweethome3d.swing.VideoPanel.VideoDialogY");
			moveVisualProperty("com.eteks.sweethome3d.swing.HomeComponent3D.detachedViewX");
			moveVisualProperty("com.eteks.sweethome3d.swing.HomeComponent3D.detachedViewY");
			moveVisualProperty("com.eteks.sweethome3d.swing.HomeComponent3D.detachedViewWidth");
			moveVisualProperty("com.eteks.sweethome3d.swing.HomeComponent3D.detachedViewHeight");
			moveVisualProperty("com.eteks.sweethome3d.swing.HomeComponent3D.detachedView");
			moveVisualProperty("com.eteks.sweethome3d.swing.HomeComponent3D.detachedViewDividerLocation");
			moveVisualProperty("com.eteks.sweethome3d.SweetHome3D.MainPaneDividerLocation");
			moveVisualProperty("com.eteks.sweethome3d.SweetHome3D.CatalogPaneDividerLocation");
			moveVisualProperty("com.eteks.sweethome3d.SweetHome3D.PlanPaneDividerLocation");
			moveVisualProperty("com.eteks.sweethome3d.SweetHome3D.PlanViewportX");
			moveVisualProperty("com.eteks.sweethome3d.SweetHome3D.PlanViewportY");
			moveVisualProperty("com.eteks.sweethome3d.SweetHome3D.FurnitureViewportY");
			moveVisualProperty("com.eteks.sweethome3d.SweetHome3D.PlanScale");
			moveVisualProperty("com.eteks.sweethome3d.SweetHome3D.ExpandedGroups");
			moveVisualProperty("com.eteks.sweethome3d.SweetHome3D.FrameX");
			moveVisualProperty("com.eteks.sweethome3d.SweetHome3D.FrameY");
			moveVisualProperty("com.eteks.sweethome3d.SweetHome3D.FrameWidth");
			moveVisualProperty("com.eteks.sweethome3d.SweetHome3D.FrameHeight");
			moveVisualProperty("com.eteks.sweethome3d.SweetHome3D.FrameMaximized");
			moveVisualProperty("com.eteks.sweethome3d.SweetHome3D.ScreenWidth");
			moveVisualProperty("com.eteks.sweethome3d.SweetHome3D.ScreenHeight");
		}
		
		addModelListeners();
	}
	
	private void moveVisualProperty(String visualPropertyName)
	{
		if (this.visualProperties.containsKey(visualPropertyName))
		{
			Object value = this.visualProperties.get(visualPropertyName);
			this.properties.put(visualPropertyName, value != null ? String.valueOf(value) : null);
			this.visualProperties.remove(visualPropertyName);
		}
	}
	
	private void init(boolean newHome)
	{
		// Initialize transient lists
		this.selectedItems = new ArrayList<Selectable>();
		initListenersSupport(this);
		
		if (this.furnitureVisibleProperties == null)
		{
			// Set the furniture properties that were visible before version 0.19 
			this.furnitureVisibleProperties = Arrays.asList(new HomePieceOfFurniture.SortableProperty[] {
					HomePieceOfFurniture.SortableProperty.NAME, HomePieceOfFurniture.SortableProperty.WIDTH,
					HomePieceOfFurniture.SortableProperty.DEPTH, HomePieceOfFurniture.SortableProperty.HEIGHT,
					HomePieceOfFurniture.SortableProperty.COLOR, HomePieceOfFurniture.SortableProperty.MOVABLE,
					HomePieceOfFurniture.SortableProperty.DOOR_OR_WINDOW,
					HomePieceOfFurniture.SortableProperty.VISIBLE });
		}
		// Create a default top camera that matches default point of view 
		this.topCamera = new Camera(50, 1050, 1010, (float) Math.PI, (float) Math.PI / 4, (float) Math.PI * 63 / 180);
		// Create a default observer camera (use a 63� field of view equivalent to a 35mm lens for a 24x36 film)
		this.observerCamera = new ObserverCamera(50, 50, 170, 7 * (float) Math.PI / 4, (float) Math.PI / 16,
				(float) Math.PI * 63 / 180);
		this.storedCameras = Collections.emptyList();
		// Initialize new fields 
		this.environment = new HomeEnvironment();
		this.rooms = new ArrayList<Room>();
		this.polylines = new ArrayList<Polyline>();
		this.dimensionLines = new ArrayList<DimensionLine>();
		this.labels = new ArrayList<Label>();
		this.compass = new Compass(-100, 50, 100);
		this.levels = new ArrayList<Level>();
		// Let compass be visible only on new homes
		this.compass.setVisible(newHome);
		this.visualProperties = new HashMap<String, Object>();
		this.properties = new HashMap<String, String>();
		
		this.version = CURRENT_VERSION;
	}
	
	private static void initListenersSupport(Home home)
	{
		home.furnitureChangeSupport = new CollectionChangeSupport<HomePieceOfFurniture>(home);
		home.selectionListeners = new ArrayList<SelectionListener>();
		home.levelsChangeSupport = new CollectionChangeSupport<Level>(home);
		home.wallsChangeSupport = new CollectionChangeSupport<Wall>(home);
		home.roomsChangeSupport = new CollectionChangeSupport<Room>(home);
		home.polylinesChangeSupport = new CollectionChangeSupport<Polyline>(home);
		home.dimensionLinesChangeSupport = new CollectionChangeSupport<DimensionLine>(home);
		home.labelsChangeSupport = new CollectionChangeSupport<Label>(home);
		home.propertyChangeSupport = new PropertyChangeSupport(home);
	}
	
	/**
	 * Adds listeners to model.
	 */
	private void addModelListeners()
	{
		// Add listeners to levels to maintain its elevation order
		final PropertyChangeListener levelElevationChangeListener = new PropertyChangeListener()
		{
			public void propertyChange(PropertyChangeEvent ev)
			{
				if (Level.Property.ELEVATION.name().equals(ev.getPropertyName())
						|| Level.Property.ELEVATION_INDEX.name().equals(ev.getPropertyName()))
				{
					levels = new ArrayList<Level>(levels);
					Collections.sort(levels, LEVEL_ELEVATION_COMPARATOR);
				}
			}
		};
		for (Level level : this.levels)
		{
			level.addPropertyChangeListener(levelElevationChangeListener);
		}
		addLevelsListener(new CollectionListener<Level>()
		{
			public void collectionChanged(CollectionEvent<Level> ev)
			{
				switch (ev.getType())
				{
					case ADD:
						ev.getItem().addPropertyChangeListener(levelElevationChangeListener);
						break;
					case DELETE:
						ev.getItem().removePropertyChangeListener(levelElevationChangeListener);
						break;
				}
			}
		});
	}
	
	/**
	 * Sets the version of this home and writes it to <code>out</code> stream
	 * with default writing method. 
	 */
	private void writeObject(java.io.ObjectOutputStream out) throws IOException
	{
		this.version = CURRENT_VERSION;
		
		if (KEEP_BACKWARD_COMPATIBLITY)
		{
			HomePieceOfFurniture.SortableProperty homeFurnitureSortedProperty = this.furnitureSortedProperty;
			List<HomePieceOfFurniture.SortableProperty> homeFurnitureVisibleProperties = this.furnitureVisibleProperties;
			List<HomePieceOfFurniture> homeFurniture = this.furniture;
			try
			{
				if (this.furnitureSortedProperty != null)
				{
					this.furnitureSortedPropertyName = this.furnitureSortedProperty.name();
					// Store in furnitureSortedProperty only backward compatible property
					if (!isFurnitureSortedPropertyBackwardCompatible(this.furnitureSortedProperty))
					{
						this.furnitureSortedProperty = null;
					}
				}
				
				this.furnitureVisiblePropertyNames = new ArrayList<String>();
				// Store in furnitureVisibleProperties only backward compatible properties
				this.furnitureVisibleProperties = new ArrayList<HomePieceOfFurniture.SortableProperty>();
				for (HomePieceOfFurniture.SortableProperty visibleProperty : homeFurnitureVisibleProperties)
				{
					this.furnitureVisiblePropertyNames.add(visibleProperty.name());
					if (isFurnitureSortedPropertyBackwardCompatible(visibleProperty))
					{
						this.furnitureVisibleProperties.add(visibleProperty);
					}
				}
				
				// Store referenced HomeFurnitureGroup instances in a separate field 
				// for backward compatibility reasons (version < 2.3)
				this.furnitureWithGroups = this.furniture;
				// Serialize a furnitureWithDoorsAndWindows field that contains only 
				// HomePieceOfFurniture, HomeDoorOrWindow and HomeLight instances
				// for backward compatibility reasons (version < 1.7)
				this.furnitureWithDoorsAndWindows = new ArrayList<HomePieceOfFurniture>(this.furniture.size());
				// Serialize a furniture field that contains only HomePieceOfFurniture instances
				this.furniture = new ArrayList<HomePieceOfFurniture>(this.furniture.size());
				for (HomePieceOfFurniture piece : this.furnitureWithGroups)
				{
					if (piece.getClass() == HomePieceOfFurniture.class)
					{
						this.furnitureWithDoorsAndWindows.add(piece);
						this.furniture.add(piece);
					}
					else
					{
						if (piece.getClass() == HomeFurnitureGroup.class)
						{
							// Add the ungrouped pieces to furniture and furnitureWithDoorsAndWindows list 
							for (HomePieceOfFurniture groupPiece : getGroupFurniture((HomeFurnitureGroup) piece))
							{
								this.furnitureWithDoorsAndWindows.add(groupPiece);
								if (groupPiece.getClass() == HomePieceOfFurniture.class)
								{
									this.furniture.add(groupPiece);
								}
								else
								{
									// Create backward compatible instances
									this.furniture.add(new HomePieceOfFurniture(groupPiece));
								}
							}
						}
						else
						{
							this.furnitureWithDoorsAndWindows.add(piece);
							// Create backward compatible instances
							this.furniture.add(new HomePieceOfFurniture(piece));
						}
					}
				}
				
				// Store environment fields in home fields for compatibility reasons
				this.groundColor = this.environment.getGroundColor();
				this.groundTexture = this.environment.getGroundTexture();
				this.skyColor = this.environment.getSkyColor();
				this.lightColor = this.environment.getLightColor();
				this.wallsAlpha = this.environment.getWallsAlpha();
				
				out.defaultWriteObject();
			}
			finally
			{
				// Restore home values
				this.furniture = homeFurniture;
				this.furnitureWithDoorsAndWindows = null;
				this.furnitureWithGroups = null;
				
				this.furnitureSortedProperty = homeFurnitureSortedProperty;
				this.furnitureVisibleProperties = homeFurnitureVisibleProperties;
				// Set furnitureSortedPropertyName and furnitureVisiblePropertyNames to null
				// (they are used only for serialization)
				this.furnitureSortedPropertyName = null;
				this.furnitureVisiblePropertyNames = null;
			}
		}
		else
		{
			out.defaultWriteObject();
		}
	}
	
	/**
	 * Returns <code>true</code> if the given <code>property</code> is compatible 
	 * with the first set of sortable properties that existed in <code>HomePieceOfFurniture</code> class.
	 */
	private boolean isFurnitureSortedPropertyBackwardCompatible(HomePieceOfFurniture.SortableProperty property)
	{
		switch (property)
		{
			case NAME:
			case WIDTH:
			case DEPTH:
			case HEIGHT:
			case MOVABLE:
			case DOOR_OR_WINDOW:
			case COLOR:
			case VISIBLE:
			case X:
			case Y:
			case ELEVATION:
			case ANGLE:
				return true;
			default:
				return false;
		}
	}
	
	/**
	 * Returns all the pieces of the given <code>furnitureGroup</code>.  
	 */
	private List<HomePieceOfFurniture> getGroupFurniture(HomeFurnitureGroup furnitureGroup)
	{
		List<HomePieceOfFurniture> groupFurniture = new ArrayList<HomePieceOfFurniture>();
		for (HomePieceOfFurniture piece : furnitureGroup.getFurniture())
		{
			if (piece instanceof HomeFurnitureGroup)
			{
				groupFurniture.addAll(getGroupFurniture((HomeFurnitureGroup) piece));
			}
			else
			{
				groupFurniture.add(piece);
			}
		}
		return groupFurniture;
	}
	
	/**
	 * Adds the level <code>listener</code> in parameter to this home.
	 * @since 3.4
	 */
	public void addLevelsListener(CollectionListener<Level> listener)
	{
		this.levelsChangeSupport.addCollectionListener(listener);
	}
	
	/**
	 * Removes the level <code>listener</code> in parameter from this home.
	 * @since 3.4
	 */
	public void removeLevelsListener(CollectionListener<Level> listener)
	{
		this.levelsChangeSupport.removeCollectionListener(listener);
	}
	
	/**
	 * Returns an unmodifiable collection of the levels of this home.
	 * @since 3.4
	 */
	public List<Level> getLevels()
	{
		return Collections.unmodifiableList(this.levels);
	}
	
	/**
	 * Adds the given <code>level</code> to the list of levels of this home.
	 * Once the <code>level</code> is added, level listeners added to this home will receive a
	 * {@link CollectionListener#collectionChanged(CollectionEvent) collectionChanged}
	 * notification, with an {@link CollectionEvent#getType() event type} 
	 * equal to {@link CollectionEvent.Type#ADD ADD}. 
	 * @since 3.4
	 */
	public void addLevel(Level level)
	{
		if (level.getElevationIndex() < 0)
		{
			// Search elevation index of the added level
			int elevationIndex = 0;
			for (Level homeLevel : this.levels)
			{
				if (homeLevel.getElevation() == level.getElevation())
				{
					elevationIndex = homeLevel.getElevationIndex() + 1;
				}
				else if (homeLevel.getElevation() > level.getElevation())
				{
					break;
				}
			}
			level.setElevationIndex(elevationIndex);
		}
		// Make a copy of the list to avoid conflicts in the list returned by getLevels
		this.levels = new ArrayList<Level>(this.levels);
		// Search at which index should be inserted the new level
		int index = Collections.binarySearch(this.levels, level, LEVEL_ELEVATION_COMPARATOR);
		int levelIndex;
		if (index >= 0)
		{
			levelIndex = index;
		}
		else
		{
			levelIndex = -(index + 1);
		}
		this.levels.add(levelIndex, level);
		this.levelsChangeSupport.fireCollectionChanged(level, levelIndex, CollectionEvent.Type.ADD);
	}
	
	/**
	 * Removes the given <code>level</code> from the set of levels of this home 
	 * and all the furniture, walls, rooms, dimension lines and labels that belong to this level.
	 * Once the <code>level</code> is removed, level listeners added to this home will receive a
	 * {@link CollectionListener#collectionChanged(CollectionEvent) collectionChanged}
	 * notification, with an {@link CollectionEvent#getType() event type} 
	 * equal to {@link CollectionEvent.Type#DELETE DELETE}.
	 * @since 3.4
	 */
	public void deleteLevel(Level level)
	{
		int index = this.levels.indexOf(level);
		if (index != -1)
		{
			for (HomePieceOfFurniture piece : this.furniture)
			{
				if (piece.getLevel() == level)
				{
					deletePieceOfFurniture(piece);
				}
			}
			for (Room room : this.rooms)
			{
				if (room.getLevel() == level)
				{
					deleteRoom(room);
				}
			}
			for (Wall wall : this.walls)
			{
				if (wall.getLevel() == level)
				{
					deleteWall(wall);
				}
			}
			for (Polyline polyline : this.polylines)
			{
				if (polyline.getLevel() == level)
				{
					deletePolyline(polyline);
				}
			}
			for (DimensionLine dimensionLine : this.dimensionLines)
			{
				if (dimensionLine.getLevel() == level)
				{
					deleteDimensionLine(dimensionLine);
				}
			}
			for (Label label : this.labels)
			{
				if (label.getLevel() == level)
				{
					deleteLabel(label);
				}
			}
			if (this.selectedLevel == level)
			{
				if (this.levels.size() == 1)
				{
					setSelectedLevel(null);
					setAllLevelsSelection(false);
				}
				else
				{
					setSelectedLevel(this.levels.get(index >= 1 ? index - 1 : index + 1));
				}
			}
			// Make a copy of the list to avoid conflicts in the list returned by getLevels
			this.levels = new ArrayList<Level>(this.levels);
			this.levels.remove(index);
			this.levelsChangeSupport.fireCollectionChanged(level, index, CollectionEvent.Type.DELETE);
		}
	}
	
	/**
	 * Returns the selected level in home or <code>null</code> if home has no level.
	 * @since 3.4
	 */
	public Level getSelectedLevel()
	{
		return this.selectedLevel;
	}
	
	/**
	 * Sets the selected level in home and notifies listeners of the change.
	 * @since 3.4
	 */
	public void setSelectedLevel(Level selectedLevel)
	{
		if (selectedLevel != this.selectedLevel)
		{
			Level oldSelectedLevel = this.selectedLevel;
			this.selectedLevel = selectedLevel;
			this.propertyChangeSupport.firePropertyChange(Property.SELECTED_LEVEL.name(), oldSelectedLevel,
					selectedLevel);
		}
	}
	
	/**
	 * Returns <code>true</code> if the selected items in this home are from all levels.
	 * @since 4.4
	 */
	public boolean isAllLevelsSelection()
	{
		return this.allLevelsSelection;
	}
	
	/**
	 * Sets whether the selected items in this home are from all levels, and notifies listeners of the change.
	 * @since 4.4
	 */
	public void setAllLevelsSelection(boolean selectionAtAllLevels)
	{
		if (selectionAtAllLevels != this.allLevelsSelection)
		{
			this.allLevelsSelection = selectionAtAllLevels;
			this.propertyChangeSupport.firePropertyChange(Property.ALL_LEVELS_SELECTION.name(), !selectionAtAllLevels,
					selectionAtAllLevels);
		}
	}
	
	/**
	 * Adds the furniture <code>listener</code> in parameter to this home.
	 */
	public void addFurnitureListener(CollectionListener<HomePieceOfFurniture> listener)
	{
		this.furnitureChangeSupport.addCollectionListener(listener);
	}
	
	/**
	 * Removes the furniture <code>listener</code> in parameter from this home.
	 */
	public void removeFurnitureListener(CollectionListener<HomePieceOfFurniture> listener)
	{
		this.furnitureChangeSupport.removeCollectionListener(listener);
	}
	
	/**
	 * Returns an unmodifiable list of the furniture managed by this home. 
	 * This furniture in this list is always sorted in the index order they were added to home. 
	 */
	public List<HomePieceOfFurniture> getFurniture()
	{
		return Collections.unmodifiableList(this.furniture);
	}
	
	/**
	 * Adds the <code>piece</code> in parameter to this home.
	 * Once the <code>piece</code> is added, furniture listeners added to this home will receive a
	 * {@link CollectionListener#collectionChanged(CollectionEvent) collectionChanged}
	 * notification.
	 */
	public void addPieceOfFurniture(HomePieceOfFurniture piece)
	{
		addPieceOfFurniture(piece, this.furniture.size());
	}
	
	/**
	 * Adds the <code>piece</code> in parameter at a given <code>index</code>.
	 * Once the <code>piece</code> is added, furniture listeners added to this home will receive a
	 * {@link CollectionListener#collectionChanged(CollectionEvent) collectionChanged}
	 * notification.
	 */
	public void addPieceOfFurniture(HomePieceOfFurniture piece, int index)
	{
		// Make a copy of the list to avoid conflicts in the list returned by getFurniture
		this.furniture = new ArrayList<HomePieceOfFurniture>(this.furniture);
		piece.setLevel(this.selectedLevel);
		this.furniture.add(index, piece);
		this.furnitureChangeSupport.fireCollectionChanged(piece, index, CollectionEvent.Type.ADD);
	}
	
	/**
	 * Adds the <code>piece</code> in parameter at the <code>index</code> in the given <code>group</code>.
	 * Once the <code>piece</code> is added, furniture listeners added to this home will receive a
	 * {@link CollectionListener#collectionChanged(CollectionEvent) collectionChanged}
	 * notification with an event {@link CollectionEvent#getIndex() index} equal to -1.
	 */
	public void addPieceOfFurnitureToGroup(HomePieceOfFurniture piece, HomeFurnitureGroup group, int index)
	{
		piece.setLevel(this.selectedLevel);
		group.addPieceOfFurniture(piece, index);
		this.furnitureChangeSupport.fireCollectionChanged(piece, CollectionEvent.Type.ADD);
	}
	
	/**
	 * Deletes the <code>piece</code> in parameter from this home.
	 * Once the <code>piece</code> is deleted, furniture listeners added to this home will receive a
	 * {@link CollectionListener#collectionChanged(CollectionEvent) collectionChanged}
	 * notification. If the removed <code>piece</code> belongs to a group, the 
	 * {@link CollectionEvent#getIndex() index} of the event will be -1.
	 */
	public void deletePieceOfFurniture(HomePieceOfFurniture piece)
	{
		// Ensure selectedItems don't keep a reference to piece
		deselectItem(piece);
		int index = this.furniture.indexOf(piece);
		HomeFurnitureGroup group = index == -1 ? getPieceOfFurnitureGroup(piece, null, this.furniture) : null;
		if (index != -1 || group != null)
		{
			piece.setLevel(null);
			// Make a copy of the list to avoid conflicts in the list returned by getFurniture
			this.furniture = new ArrayList<HomePieceOfFurniture>(this.furniture);
			if (group != null)
			{
				group.deletePieceOfFurniture(piece);
				this.furnitureChangeSupport.fireCollectionChanged(piece, CollectionEvent.Type.DELETE);
			}
			else
			{
				this.furniture.remove(index);
				this.furnitureChangeSupport.fireCollectionChanged(piece, index, CollectionEvent.Type.DELETE);
			}
		}
	}
	
	/**
	 * Returns the furniture group that contains the given <code>piece</code> or <code>null</code> if it can't be found.
	 */
	private HomeFurnitureGroup getPieceOfFurnitureGroup(HomePieceOfFurniture piece, HomeFurnitureGroup furnitureGroup,
			List<HomePieceOfFurniture> furniture)
	{
		for (HomePieceOfFurniture homePiece : furniture)
		{
			if (homePiece.equals(piece))
			{
				return furnitureGroup;
			}
			else if (homePiece instanceof HomeFurnitureGroup)
			{
				HomeFurnitureGroup group = getPieceOfFurnitureGroup(piece, (HomeFurnitureGroup) homePiece,
						((HomeFurnitureGroup) homePiece).getFurniture());
				if (group != null)
				{
					return group;
				}
			}
		}
		return null;
	}
	
	/**
	 * Adds the selection <code>listener</code> in parameter to this home.
	 */
	public void addSelectionListener(SelectionListener listener)
	{
		this.selectionListeners.add(listener);
	}
	
	/**
	 * Removes the selection <code>listener</code> in parameter from this home.
	 */
	public void removeSelectionListener(SelectionListener listener)
	{
		this.selectionListeners.remove(listener);
	}
	
	/**
	 * Returns an unmodifiable list of the selected items in home.
	 */
	public List<Selectable> getSelectedItems()
	{
		return Collections.unmodifiableList(this.selectedItems);
	}
	
	/**
	 * Sets the selected items in home and notifies listeners selection change.
	 */
	public void setSelectedItems(List<? extends Selectable> selectedItems)
	{
		// Make a copy of the list to avoid conflicts in the list returned by getSelectedItems
		this.selectedItems = new ArrayList<Selectable>(selectedItems);
		if (!this.selectionListeners.isEmpty())
		{
			SelectionEvent selectionEvent = new SelectionEvent(this, getSelectedItems());
			// Work on a copy of selectionListeners to ensure a listener 
			// can modify safely listeners list
			SelectionListener[] listeners = this.selectionListeners
					.toArray(new SelectionListener[this.selectionListeners.size()]);
			for (SelectionListener listener : listeners)
			{
				listener.selectionChanged(selectionEvent);
			}
		}
	}
	
	/**
	 * Deselects <code>item</code> if it's selected and notifies listeners selection change.
	 * @since 2.2
	 */
	public void deselectItem(Selectable item)
	{
		int pieceSelectionIndex = this.selectedItems.indexOf(item);
		if (pieceSelectionIndex != -1)
		{
			List<Selectable> selectedItems = new ArrayList<Selectable>(getSelectedItems());
			selectedItems.remove(pieceSelectionIndex);
			setSelectedItems(selectedItems);
		}
	}
	
	/**
	 * Adds the room <code>listener</code> in parameter to this home.
	 */
	public void addRoomsListener(CollectionListener<Room> listener)
	{
		this.roomsChangeSupport.addCollectionListener(listener);
	}
	
	/**
	 * Removes the room <code>listener</code> in parameter from this home.
	 */
	public void removeRoomsListener(CollectionListener<Room> listener)
	{
		this.roomsChangeSupport.removeCollectionListener(listener);
	}
	
	/**
	 * Returns an unmodifiable collection of the rooms of this home.
	 */
	public List<Room> getRooms()
	{
		return Collections.unmodifiableList(this.rooms);
	}
	
	/**
	 * Adds the given <code>room</code> to the list of rooms of this home.
	 * Once the <code>room</code> is added, room listeners added to this home will receive a
	 * {@link CollectionListener#collectionChanged(CollectionEvent) collectionChanged}
	 * notification, with an {@link CollectionEvent#getType() event type} 
	 * equal to {@link CollectionEvent.Type#ADD ADD}. 
	 */
	public void addRoom(Room room)
	{
		addRoom(room, this.rooms.size());
	}
	
	/**
	 * Adds the <code>room</code> in parameter at a given <code>index</code>.
	 * Once the <code>room</code> is added, room listeners added to this home will receive a
	 * {@link CollectionListener#collectionChanged(CollectionEvent) collectionChanged}
	 * notification, with an {@link CollectionEvent#getType() event type} 
	 * equal to {@link CollectionEvent.Type#ADD ADD}.
	 */
	public void addRoom(Room room, int index)
	{
		// Make a copy of the list to avoid conflicts in the list returned by getRooms
		this.rooms = new ArrayList<Room>(this.rooms);
		this.rooms.add(index, room);
		room.setLevel(this.selectedLevel);
		this.roomsChangeSupport.fireCollectionChanged(room, index, CollectionEvent.Type.ADD);
	}
	
	/**
	 * Removes the given <code>room</code> from the set of rooms of this home.
	 * Once the <code>room</code> is removed, room listeners added to this home will receive a
	 * {@link CollectionListener#collectionChanged(CollectionEvent) collectionChanged}
	 * notification, with an {@link CollectionEvent#getType() event type} 
	 * equal to {@link CollectionEvent.Type#DELETE DELETE}.
	 */
	public void deleteRoom(Room room)
	{
		//  Ensure selectedItems don't keep a reference to room
		deselectItem(room);
		int index = this.rooms.indexOf(room);
		if (index != -1)
		{
			room.setLevel(null);
			// Make a copy of the list to avoid conflicts in the list returned by getRooms
			this.rooms = new ArrayList<Room>(this.rooms);
			this.rooms.remove(index);
			this.roomsChangeSupport.fireCollectionChanged(room, index, CollectionEvent.Type.DELETE);
		}
	}
	
	/**
	 * Adds the wall <code>listener</code> in parameter to this home.
	 */
	public void addWallsListener(CollectionListener<Wall> listener)
	{
		this.wallsChangeSupport.addCollectionListener(listener);
	}
	
	/**
	 * Removes the wall <code>listener</code> in parameter from this home.
	 */
	public void removeWallsListener(CollectionListener<Wall> listener)
	{
		this.wallsChangeSupport.removeCollectionListener(listener);
	}
	
	/**
	 * Returns an unmodifiable collection of the walls of this home.
	 */
	public Collection<Wall> getWalls()
	{
		return Collections.unmodifiableCollection(this.walls);
	}
	
	/**
	 * Adds the given <code>wall</code> to the set of walls of this home.
	 * Once the <code>wall</code> is added, wall listeners added to this home will receive a
	 * {@link CollectionListener#collectionChanged(CollectionEvent) collectionChanged}
	 * notification, with an {@link CollectionEvent#getType() event type} 
	 * equal to {@link CollectionEvent.Type#ADD ADD}. 
	 */
	public void addWall(Wall wall)
	{
		// Make a copy of the list to avoid conflicts in the list returned by getWalls
		this.walls = new ArrayList<Wall>(this.walls);
		this.walls.add(wall);
		wall.setLevel(this.selectedLevel);
		this.wallsChangeSupport.fireCollectionChanged(wall, CollectionEvent.Type.ADD);
	}
	
	/**
	 * Removes the given <code>wall</code> from the set of walls of this home.
	 * Once the <code>wall</code> is removed, wall listeners added to this home will receive a
	 * {@link CollectionListener#collectionChanged(CollectionEvent) collectionChanged}
	 * notification, with an {@link CollectionEvent#getType() event type} 
	 * equal to {@link CollectionEvent.Type#DELETE DELETE}.
	 * If any wall is attached to <code>wall</code> they will be detached from it.
	 */
	public void deleteWall(Wall wall)
	{
		//  Ensure selectedItems don't keep a reference to wall
		deselectItem(wall);
		// Detach any other wall attached to wall
		for (Wall otherWall : getWalls())
		{
			if (wall.equals(otherWall.getWallAtStart()))
			{
				otherWall.setWallAtStart(null);
			}
			else if (wall.equals(otherWall.getWallAtEnd()))
			{
				otherWall.setWallAtEnd(null);
			}
		}
		int index = this.walls.indexOf(wall);
		if (index != -1)
		{
			wall.setLevel(null);
			// Make a copy of the list to avoid conflicts in the list returned by getWalls
			this.walls = new ArrayList<Wall>(this.walls);
			this.walls.remove(index);
			this.wallsChangeSupport.fireCollectionChanged(wall, CollectionEvent.Type.DELETE);
		}
	}
	
	/**
	 * Adds the polyline <code>listener</code> in parameter to this home.
	 * @since 5.0
	 */
	public void addPolylinesListener(CollectionListener<Polyline> listener)
	{
		this.polylinesChangeSupport.addCollectionListener(listener);
	}
	
	/**
	 * Removes the polyline <code>listener</code> in parameter from this home.
	 * @since 5.0
	 */
	public void removePolylinesListener(CollectionListener<Polyline> listener)
	{
		this.polylinesChangeSupport.removeCollectionListener(listener);
	}
	
	/**
	 * Returns an unmodifiable collection of the polylines of this home.
	 * @since 5.0
	 */
	public List<Polyline> getPolylines()
	{
		return Collections.unmodifiableList(this.polylines);
	}
	
	/**
	 * Adds a given <code>polyline</code> to the set of polylines of this home.
	 * Once the <code>polyline</code> is added, polyline listeners added to this home will receive a
	 * {@link CollectionListener#collectionChanged(CollectionEvent) collectionChanged}
	 * notification, with an {@link CollectionEvent#getType() event type} 
	 * equal to {@link CollectionEvent.Type#ADD ADD}. 
	 * @since 5.0
	 */
	public void addPolyline(Polyline polyline)
	{
		addPolyline(polyline, this.polylines.size());
	}
	
	/**
	 * Adds a <code>polyline</code> at a given <code>index</code> of the set of polylines of this home.
	 * Once the <code>polyline</code> is added, polyline listeners added to this home will receive a
	 * {@link CollectionListener#collectionChanged(CollectionEvent) collectionChanged}
	 * notification, with an {@link CollectionEvent#getType() event type} 
	 * equal to {@link CollectionEvent.Type#ADD ADD}. 
	 * @since 5.0
	 */
	public void addPolyline(Polyline polyline, int index)
	{
		// Make a copy of the list to avoid conflicts in the list returned by getPolylines
		this.polylines = new ArrayList<Polyline>(this.polylines);
		this.polylines.add(index, polyline);
		polyline.setLevel(this.selectedLevel);
		this.polylinesChangeSupport.fireCollectionChanged(polyline, CollectionEvent.Type.ADD);
	}
	
	/**
	 * Removes a given <code>polyline</code> from the set of polylines of this home.
	 * Once the <code>polyline</code> is removed, polyline listeners added to this home will receive a
	 * {@link CollectionListener#collectionChanged(CollectionEvent) collectionChanged}
	 * notification, with an {@link CollectionEvent#getType() event type} 
	 * equal to {@link CollectionEvent.Type#DELETE DELETE}.
	 * @since 5.0
	 */
	public void deletePolyline(Polyline polyline)
	{
		//  Ensure selectedItems don't keep a reference to polyline
		deselectItem(polyline);
		int index = this.polylines.indexOf(polyline);
		if (index != -1)
		{
			polyline.setLevel(null);
			// Make a copy of the list to avoid conflicts in the list returned by getPolylines
			this.polylines = new ArrayList<Polyline>(this.polylines);
			this.polylines.remove(index);
			this.polylinesChangeSupport.fireCollectionChanged(polyline, CollectionEvent.Type.DELETE);
		}
	}
	
	/**
	 * Adds the dimension line <code>listener</code> in parameter to this home.
	 */
	public void addDimensionLinesListener(CollectionListener<DimensionLine> listener)
	{
		this.dimensionLinesChangeSupport.addCollectionListener(listener);
	}
	
	/**
	 * Removes the dimension line <code>listener</code> in parameter from this home.
	 */
	public void removeDimensionLinesListener(CollectionListener<DimensionLine> listener)
	{
		this.dimensionLinesChangeSupport.removeCollectionListener(listener);
	}
	
	/**
	 * Returns an unmodifiable collection of the dimension lines of this home.
	 */
	public Collection<DimensionLine> getDimensionLines()
	{
		return Collections.unmodifiableCollection(this.dimensionLines);
	}
	
	/**
	 * Adds the given dimension line to the set of dimension lines of this home.
	 * Once <code>dimensionLine</code> is added, dimension line listeners added 
	 * to this home will receive a
	 * {@link CollectionListener#collectionChanged(CollectionEvent) collectionChanged}
	 * notification, with an {@link CollectionEvent#getType() event type} 
	 * equal to {@link CollectionEvent.Type#ADD ADD}. 
	 */
	public void addDimensionLine(DimensionLine dimensionLine)
	{
		// Make a copy of the list to avoid conflicts in the list returned by getDimensionLines
		this.dimensionLines = new ArrayList<DimensionLine>(this.dimensionLines);
		this.dimensionLines.add(dimensionLine);
		dimensionLine.setLevel(this.selectedLevel);
		this.dimensionLinesChangeSupport.fireCollectionChanged(dimensionLine, CollectionEvent.Type.ADD);
	}
	
	/**
	 * Removes the given dimension line from the set of dimension lines of this home.
	 * Once <code>dimensionLine</code> is removed, dimension line listeners added 
	 * to this home will receive a
	 * {@link CollectionListener#collectionChanged(CollectionEvent) collectionChanged}
	 * notification, with an {@link CollectionEvent#getType() event type} 
	 * equal to {@link CollectionEvent.Type#DELETE DELETE}.
	 */
	public void deleteDimensionLine(DimensionLine dimensionLine)
	{
		//  Ensure selectedItems don't keep a reference to dimension line
		deselectItem(dimensionLine);
		int index = this.dimensionLines.indexOf(dimensionLine);
		if (index != -1)
		{
			dimensionLine.setLevel(null);
			// Make a copy of the list to avoid conflicts in the list returned by getDimensionLines
			this.dimensionLines = new ArrayList<DimensionLine>(this.dimensionLines);
			this.dimensionLines.remove(index);
			this.dimensionLinesChangeSupport.fireCollectionChanged(dimensionLine, CollectionEvent.Type.DELETE);
		}
	}
	
	/**
	 * Adds the label <code>listener</code> in parameter to this home.
	 */
	public void addLabelsListener(CollectionListener<Label> listener)
	{
		this.labelsChangeSupport.addCollectionListener(listener);
	}
	
	/**
	 * Removes the label <code>listener</code> in parameter from this home.
	 */
	public void removeLabelsListener(CollectionListener<Label> listener)
	{
		this.labelsChangeSupport.removeCollectionListener(listener);
	}
	
	/**
	 * Returns an unmodifiable collection of the labels of this home.
	 */
	public Collection<Label> getLabels()
	{
		return Collections.unmodifiableCollection(this.labels);
	}
	
	/**
	 * Adds the given label to the set of labels of this home.
	 * Once <code>label</code> is added, label listeners added 
	 * to this home will receive a
	 * {@link CollectionListener#collectionChanged(CollectionEvent) collectionChanged}
	 * notification, with an {@link CollectionEvent#getType() event type} 
	 * equal to {@link CollectionEvent.Type#ADD ADD}. 
	 */
	public void addLabel(Label label)
	{
		// Make a copy of the list to avoid conflicts in the list returned by getLabels
		this.labels = new ArrayList<Label>(this.labels);
		this.labels.add(label);
		label.setLevel(this.selectedLevel);
		this.labelsChangeSupport.fireCollectionChanged(label, CollectionEvent.Type.ADD);
	}
	
	/**
	 * Removes the given label from the set of labels of this home.
	 * Once <code>label</code> is removed, label listeners added to this home will receive a
	 * {@link CollectionListener#collectionChanged(CollectionEvent) collectionChanged}
	 * notification, with an {@link CollectionEvent#getType() event type} 
	 * equal to {@link CollectionEvent.Type#DELETE DELETE}.
	 */
	public void deleteLabel(Label label)
	{
		//  Ensure selectedItems don't keep a reference to label
		deselectItem(label);
		int index = this.labels.indexOf(label);
		if (index != -1)
		{
			label.setLevel(null);
			// Make a copy of the list to avoid conflicts in the list returned by getLabels
			this.labels = new ArrayList<Label>(this.labels);
			this.labels.remove(index);
			this.labelsChangeSupport.fireCollectionChanged(label, CollectionEvent.Type.DELETE);
		}
	}
	
	/**
	 * Returns all the selectable and viewable items in this home, except the observer camera.
	 * @return a list containing viewable walls, furniture, dimension lines, labels and compass.
	 * @since 5.0
	 */
	public List<Selectable> getSelectableViewableItems()
	{
		List<Selectable> homeItems = new ArrayList<Selectable>();
		addViewableItems(this.walls, homeItems);
		addViewableItems(this.rooms, homeItems);
		addViewableItems(this.dimensionLines, homeItems);
		addViewableItems(this.polylines, homeItems);
		addViewableItems(this.labels, homeItems);
		for (HomePieceOfFurniture piece : getFurniture())
		{
			if (piece.isVisible() && (piece.getLevel() == null || piece.getLevel().isViewable()))
			{
				homeItems.add(piece);
			}
		}
		if (getCompass().isVisible())
		{
			homeItems.add(getCompass());
		}
		return homeItems;
	}
	
	/**
	 * Adds the viewable items to the set of selectable viewable items.
	 */
	private <T extends Selectable> void addViewableItems(Collection<T> items, List<Selectable> selectableViewableItems)
	{
		for (T item : items)
		{
			if (item instanceof Elevatable)
			{
				Elevatable elevatableItem = (Elevatable) item;
				if (elevatableItem.getLevel() == null || elevatableItem.getLevel().isViewable())
				{
					selectableViewableItems.add(item);
				}
			}
		}
	}
	
	/**
	 * Returns <code>true</code> if this home doesn't contain any item i.e.  
	 * no piece of furniture, no wall, no room, no dimension line and no label.
	 * @since 2.2
	 */
	public boolean isEmpty()
	{
		return this.furniture.isEmpty() && this.walls.isEmpty() && this.rooms.isEmpty() && this.dimensionLines.isEmpty()
				&& this.polylines.isEmpty() && this.labels.isEmpty();
	}
	
	/**
	 * Adds the property change <code>listener</code> in parameter to this home.
	 */
	public void addPropertyChangeListener(Property property, PropertyChangeListener listener)
	{
		this.propertyChangeSupport.addPropertyChangeListener(property.name(), listener);
	}
	
	/**
	 * Removes the property change <code>listener</code> in parameter from this home.
	 */
	public void removePropertyChangeListener(Property property, PropertyChangeListener listener)
	{
		this.propertyChangeSupport.removePropertyChangeListener(property.name(), listener);
	}
	
	/**
	 * Returns the wall height of this home.
	 */
	public float getWallHeight()
	{
		return this.wallHeight;
	}
	
	/**
	 * Returns the name of this home.
	 */
	public String getName()
	{
		return this.name;
	}
	
	/**
	 * Sets the name of this home and fires a <code>PropertyChangeEvent</code>.
	 */
	public void setName(String name)
	{
		if (name != this.name && (name == null || !name.equals(this.name)))
		{
			String oldName = this.name;
			this.name = name;
			this.propertyChangeSupport.firePropertyChange(Property.NAME.name(), oldName, name);
		}
	}
	
	/**
	 * Returns whether the state of this home is modified or not.
	 */
	public boolean isModified()
	{
		return this.modified;
	}
	
	/**
	 * Sets the modified state of this home and fires a <code>PropertyChangeEvent</code>.
	 */
	public void setModified(boolean modified)
	{
		if (modified != this.modified)
		{
			this.modified = modified;
			this.propertyChangeSupport.firePropertyChange(Property.MODIFIED.name(), !modified, modified);
		}
	}
	
	/**
	 * Returns whether this home was recovered or not.
	 * @since 3.0
	 */
	public boolean isRecovered()
	{
		return this.recovered;
	}
	
	/**
	 * Sets whether this home was recovered or not and fires a <code>PropertyChangeEvent</code>.
	 * @since 3.0
	 */
	public void setRecovered(boolean recovered)
	{
		if (recovered != this.recovered)
		{
			this.recovered = recovered;
			this.propertyChangeSupport.firePropertyChange(Property.RECOVERED.name(), !recovered, recovered);
		}
	}
	
	/**
	 * Returns whether this home was repaired or not.
	 * @since 4.4
	 */
	public boolean isRepaired()
	{
		return this.repaired;
	}
	
	/**
	 * Sets whether this home is repaired or not and fires a <code>PropertyChangeEvent</code>.
	 * @since 4.4
	 */
	public void setRepaired(boolean repaired)
	{
		if (repaired != this.repaired)
		{
			this.repaired = repaired;
			this.propertyChangeSupport.firePropertyChange(Property.REPAIRED.name(), !repaired, repaired);
		}
	}
	
	/**
	 * Returns the furniture property on which home is sorted or <code>null</code> if
	 * home furniture isn't sorted.
	 */
	public HomePieceOfFurniture.SortableProperty getFurnitureSortedProperty()
	{
		return this.furnitureSortedProperty;
	}
	
	/**
	 * Sets the furniture property on which this home should be sorted 
	 * and fires a <code>PropertyChangeEvent</code>.
	 */
	public void setFurnitureSortedProperty(HomePieceOfFurniture.SortableProperty furnitureSortedProperty)
	{
		if (furnitureSortedProperty != this.furnitureSortedProperty
				&& (furnitureSortedProperty == null || !furnitureSortedProperty.equals(this.furnitureSortedProperty)))
		{
			HomePieceOfFurniture.SortableProperty oldFurnitureSortedProperty = this.furnitureSortedProperty;
			this.furnitureSortedProperty = furnitureSortedProperty;
			this.propertyChangeSupport.firePropertyChange(Property.FURNITURE_SORTED_PROPERTY.name(),
					oldFurnitureSortedProperty, furnitureSortedProperty);
		}
	}
	
	/**
	 * Returns whether furniture is sorted in ascending or descending order.
	 */
	public boolean isFurnitureDescendingSorted()
	{
		return this.furnitureDescendingSorted;
	}
	
	/**
	 * Sets the furniture sort order on which home should be sorted 
	 * and fires a <code>PropertyChangeEvent</code>.
	 */
	public void setFurnitureDescendingSorted(boolean furnitureDescendingSorted)
	{
		if (furnitureDescendingSorted != this.furnitureDescendingSorted)
		{
			this.furnitureDescendingSorted = furnitureDescendingSorted;
			this.propertyChangeSupport.firePropertyChange(Property.FURNITURE_DESCENDING_SORTED.name(),
					!furnitureDescendingSorted, furnitureDescendingSorted);
		}
	}
	
	/**
	 * Returns an unmodifiable list of the furniture properties that are visible.
	 */
	public List<HomePieceOfFurniture.SortableProperty> getFurnitureVisibleProperties()
	{
		if (this.furnitureVisibleProperties == null)
		{
			return Collections.emptyList();
		}
		else
		{
			return Collections.unmodifiableList(this.furnitureVisibleProperties);
		}
	}
	
	/**
	 * Sets the furniture properties that are visible and the order in which they are visible,
	 * then fires a <code>PropertyChangeEvent</code>.
	 */
	public void setFurnitureVisibleProperties(List<HomePieceOfFurniture.SortableProperty> furnitureVisibleProperties)
	{
		if (furnitureVisibleProperties != this.furnitureVisibleProperties && (furnitureVisibleProperties == null
				|| !furnitureVisibleProperties.equals(this.furnitureVisibleProperties)))
		{
			List<HomePieceOfFurniture.SortableProperty> oldFurnitureVisibleProperties = this.furnitureVisibleProperties;
			this.furnitureVisibleProperties = new ArrayList<HomePieceOfFurniture.SortableProperty>(
					furnitureVisibleProperties);
			this.propertyChangeSupport.firePropertyChange(Property.FURNITURE_VISIBLE_PROPERTIES.name(),
					Collections.unmodifiableList(oldFurnitureVisibleProperties),
					Collections.unmodifiableList(furnitureVisibleProperties));
		}
	}
	
	/**
	 * Returns the plan background image of this home.
	 */
	public BackgroundImage getBackgroundImage()
	{
		return this.backgroundImage;
	}
	
	/**
	 * Sets the plan background image of this home and fires a <code>PropertyChangeEvent</code>.
	 */
	public void setBackgroundImage(BackgroundImage backgroundImage)
	{
		if (backgroundImage != this.backgroundImage)
		{
			BackgroundImage oldBackgroundImage = this.backgroundImage;
			this.backgroundImage = backgroundImage;
			this.propertyChangeSupport.firePropertyChange(Property.BACKGROUND_IMAGE.name(), oldBackgroundImage,
					backgroundImage);
		}
	}
	
	/**
	 * Returns the camera used to display this home from a top point of view.
	 */
	public Camera getTopCamera()
	{
		return this.topCamera;
	}
	
	/**
	 * Returns the camera used to display this home from an observer point of view.
	 */
	public ObserverCamera getObserverCamera()
	{
		return this.observerCamera;
	}
	
	/**
	 * Sets the camera used to display this home and fires a <code>PropertyChangeEvent</code>.
	 */
	public void setCamera(Camera camera)
	{
		if (camera != this.camera)
		{
			Camera oldCamera = this.camera;
			this.camera = camera;
			this.propertyChangeSupport.firePropertyChange(Property.CAMERA.name(), oldCamera, camera);
		}
	}
	
	/**
	 * Returns the camera used to display this home.
	 */
	public Camera getCamera()
	{
		if (this.camera == null)
		{
			// Use by default top camera
			this.camera = getTopCamera();
		}
		return this.camera;
	}
	
	/**
	 * Sets the cameras stored by this home and fires a <code>PropertyChangeEvent</code>.
	 * The list given as parameter is cloned but not the camera instances it contains.
	 * @since 3.0
	 */
	public void setStoredCameras(List<Camera> storedCameras)
	{
		if (!this.storedCameras.equals(storedCameras))
		{
			List<Camera> oldStoredCameras = this.storedCameras;
			if (storedCameras == null)
			{
				this.storedCameras = Collections.emptyList();
			}
			else
			{
				this.storedCameras = new ArrayList<Camera>(storedCameras);
			}
			this.propertyChangeSupport.firePropertyChange(Property.STORED_CAMERAS.name(),
					Collections.unmodifiableList(oldStoredCameras), Collections.unmodifiableList(storedCameras));
		}
	}
	
	/**
	 * Returns an unmodifiable list of the cameras stored by this home.
	 * @since 3.0
	 */
	public List<Camera> getStoredCameras()
	{
		return Collections.unmodifiableList(this.storedCameras);
	}
	
	/**
	 * Returns the environment attributes of this home.
	 */
	public HomeEnvironment getEnvironment()
	{
		return this.environment;
	}
	
	/**
	 * Returns the compass associated to this home.
	 * @since 3.0
	 */
	public Compass getCompass()
	{
		return this.compass;
	}
	
	/**
	 * Returns the print attributes of this home.
	 */
	public HomePrint getPrint()
	{
		return this.print;
	}
	
	/**
	 * Sets the print attributes of this home and fires a <code>PropertyChangeEvent</code>.
	 */
	public void setPrint(HomePrint print)
	{
		if (print != this.print)
		{
			HomePrint oldPrint = this.print;
			this.print = print;
			this.propertyChangeSupport.firePropertyChange(Property.PRINT.name(), oldPrint, print);
		}
		this.print = print;
	}
	
	/**
	 * Returns the value of the visual property <code>propertyName</code> associated with this home.
	 * @deprecated {@link #getVisualProperty(String)} and {@link #setVisualProperty(String, Object)} 
	 *     should be replaced by calls to {@link #getProperty(String)} and {@link #setProperty(String, String)}
	 *     to ensure they can be easily saved and read. Future file format might not save visual properties anymore.
	 */
	public Object getVisualProperty(String propertyName)
	{
		return this.visualProperties.get(propertyName);
	}
	
	/**
	 * Sets a visual property associated with this home.
	 * @deprecated {@link #getVisualProperty(String)} and {@link #setVisualProperty(String, Object)} 
	 *     should be replaced by calls to {@link #getProperty(String)} and {@link #setProperty(String, String)}
	 *     to ensure they can be easily saved and read. Future file format might not save visual properties anymore.
	 */
	public void setVisualProperty(String propertyName, Object propertyValue)
	{
		this.visualProperties.put(propertyName, propertyValue);
	}
	
	/**
	 * Returns the value of the property <code>propertyName</code> associated with this home.
	 * @return the value of the property or <code>null</code> if it doesn't exist. 
	 * @since 5.2
	 */
	public String getProperty(String propertyName)
	{
		return this.properties.get(propertyName);
	}
	
	/**
	 * Returns the numeric value of the property <code>propertyName</code> associated with this home.
	 * @return an instance of {@link Long}, {@link Double} or <code>null</code> if the property 
	 * doesn't exist or can't be parsed. 
	 * @since 5.2
	 */
	public Number getNumericProperty(String propertyName)
	{
		String value = this.properties.get(propertyName);
		if (value != null)
		{
			try
			{
				return new Long(value);
			}
			catch (NumberFormatException ex)
			{
				try
				{
					return new Double(value);
				}
				catch (NumberFormatException ex1)
				{}
			}
		}
		return null;
	}
	
	/**
	 * Sets a property associated with this home.
	 * @since 5.2
	 */
	public void setProperty(String propertyName, String propertyValue)
	{
		if (this.properties.containsKey(propertyName) && propertyValue == null)
		{
			this.properties.remove(propertyName);
		}
		this.properties.put(propertyName, propertyValue);
	}
	
	/**
	 * Returns the property names.
	 * @return a collection of all the names of the properties set with {@link #setProperty(String, String) setProperty} 
	 * @since 5.2
	 */
	public Collection<String> getPropertyNames()
	{
		return this.properties.keySet();
	}
	
	/**
	 * Returns <code>true</code> if the home objects belonging to the base plan 
	 * (generally walls, rooms, dimension lines and texts) are locked.
	 * @since 1.8
	 */
	public boolean isBasePlanLocked()
	{
		return this.basePlanLocked;
	}
	
	/**
	 * Sets whether home objects belonging to the base plan (generally walls, rooms, 
	 * dimension lines and texts) are locked and fires a <code>PropertyChangeEvent</code>.
	 * @since 1.8
	 */
	public void setBasePlanLocked(boolean basePlanLocked)
	{
		if (basePlanLocked != this.basePlanLocked)
		{
			this.basePlanLocked = basePlanLocked;
			this.propertyChangeSupport.firePropertyChange(Property.BASE_PLAN_LOCKED.name(), !basePlanLocked,
					basePlanLocked);
		}
	}
	
	/**
	 * Returns the version of this home, the last time it was serialized or 
	 * or {@link #CURRENT_VERSION} if it is not serialized yet or 
	 * was serialized with Sweet Home 3D 0.x.  
	 * Version is useful to know with which Sweet Home 3D version this home was saved
	 * and warn user that he may lose information if he saves with 
	 * current application a home created by a more recent version.
	 */
	public long getVersion()
	{
		return this.version;
	}
	
	/**
	 * Sets the version of this home.
	 * @since 5.2
	 */
	public void setVersion(long version)
	{
		this.version = version;
	}
	
	/**
	 * Returns a clone of this home and the objects it contains. 
	 * Listeners bound to this home aren't added to the returned home.
	 * @since 2.3
	 */
	@Override
	public Home clone()
	{
		try
		{
			Home clone = (Home) super.clone();
			copyHomeData(this, clone);
			initListenersSupport(clone);
			clone.addModelListeners();
			return clone;
		}
		catch (CloneNotSupportedException ex)
		{
			throw new IllegalStateException("Super class isn't cloneable");
		}
	}
	
	/**
	 * Copies all data of a <code>source</code> home to a <code>destination</code> home.
	 */
	private static void copyHomeData(Home source, Home destination)
	{
		// Copy non mutable data
		destination.allLevelsSelection = source.allLevelsSelection;
		destination.name = source.name;
		destination.modified = source.modified;
		destination.recovered = source.recovered;
		destination.repaired = source.repaired;
		destination.backgroundImage = source.backgroundImage;
		destination.print = source.print;
		destination.furnitureDescendingSorted = source.furnitureDescendingSorted;
		destination.version = source.version;
		destination.basePlanLocked = source.basePlanLocked;
		destination.skyColor = source.skyColor;
		destination.groundColor = source.groundColor;
		destination.lightColor = source.lightColor;
		destination.wallsAlpha = source.wallsAlpha;
		destination.furnitureSortedProperty = source.furnitureSortedProperty;
		
		// Deep copy selectable items
		destination.selectedItems = new ArrayList<Selectable>(source.selectedItems.size());
		destination.furniture = cloneSelectableItems(source.furniture, source.selectedItems, destination.selectedItems);
		for (int i = 0; i < source.furniture.size(); i++)
		{
			HomePieceOfFurniture piece = source.furniture.get(i);
			if (piece instanceof HomeDoorOrWindow && ((HomeDoorOrWindow) piece).isBoundToWall())
			{
				((HomeDoorOrWindow) destination.furniture.get(i)).setBoundToWall(true);
			}
		}
		destination.rooms = cloneSelectableItems(source.rooms, source.selectedItems, destination.selectedItems);
		destination.dimensionLines = cloneSelectableItems(source.dimensionLines, source.selectedItems,
				destination.selectedItems);
		destination.polylines = cloneSelectableItems(source.polylines, source.selectedItems, destination.selectedItems);
		destination.labels = cloneSelectableItems(source.labels, source.selectedItems, destination.selectedItems);
		// Deep copy walls
		destination.walls = Wall.clone(source.walls);
		for (int i = 0; i < source.walls.size(); i++)
		{
			Wall wall = source.walls.get(i);
			if (source.selectedItems.contains(wall))
			{
				destination.selectedItems.add(destination.walls.get(i));
			}
		}
		// Clone levels and set the level of cloned objects 
		destination.levels = new ArrayList<Level>();
		if (source.levels.size() > 0)
		{
			for (Level level : source.levels)
			{
				destination.levels.add(level.clone());
			}
			for (int i = 0; i < source.furniture.size(); i++)
			{
				Level pieceLevel = source.furniture.get(i).getLevel();
				if (pieceLevel != null)
				{
					// As soon as there's more than one level, every object is supposed to have its level set
					// but as level can still be null for a undetermined reason, prefer to keep level
					// to null in the cloned object and having errors further than throwing exception here
					destination.furniture.get(i).setLevel(destination.levels.get(source.levels.indexOf(pieceLevel)));
				}
			}
			for (int i = 0; i < source.rooms.size(); i++)
			{
				Level roomLevel = source.rooms.get(i).getLevel();
				if (roomLevel != null)
				{
					destination.rooms.get(i).setLevel(destination.levels.get(source.levels.indexOf(roomLevel)));
				}
			}
			for (int i = 0; i < source.dimensionLines.size(); i++)
			{
				Level dimensionLineLevel = source.dimensionLines.get(i).getLevel();
				if (dimensionLineLevel != null)
				{
					destination.dimensionLines.get(i)
							.setLevel(destination.levels.get(source.levels.indexOf(dimensionLineLevel)));
				}
			}
			for (int i = 0; i < source.polylines.size(); i++)
			{
				Level polylineLevel = source.polylines.get(i).getLevel();
				if (polylineLevel != null)
				{
					destination.polylines.get(i).setLevel(destination.levels.get(source.levels.indexOf(polylineLevel)));
				}
			}
			for (int i = 0; i < source.labels.size(); i++)
			{
				Level labelLevel = source.labels.get(i).getLevel();
				if (labelLevel != null)
				{
					destination.labels.get(i).setLevel(destination.levels.get(source.levels.indexOf(labelLevel)));
				}
			}
			for (int i = 0; i < source.walls.size(); i++)
			{
				Level wallLevel = source.walls.get(i).getLevel();
				if (wallLevel != null)
				{
					destination.walls.get(i).setLevel(destination.levels.get(source.levels.indexOf(wallLevel)));
				}
			}
			if (source.selectedLevel != null)
			{
				destination.selectedLevel = destination.levels.get(source.levels.indexOf(source.selectedLevel));
			}
		}
		// Copy cameras
		destination.observerCamera = source.observerCamera.clone();
		destination.topCamera = source.topCamera.clone();
		if (source.camera == source.observerCamera)
		{
			destination.camera = destination.observerCamera;
			if (source.selectedItems.contains(source.observerCamera))
			{
				destination.selectedItems.add(destination.observerCamera);
			}
		}
		else
		{
			destination.camera = destination.topCamera;
		}
		destination.storedCameras = new ArrayList<Camera>(source.storedCameras.size());
		for (Camera camera : source.storedCameras)
		{
			destination.storedCameras.add(camera.clone());
		}
		// Copy other mutable objects
		destination.environment = source.environment.clone();
		destination.compass = source.compass.clone();
		destination.furnitureVisibleProperties = new ArrayList<HomePieceOfFurniture.SortableProperty>(
				source.furnitureVisibleProperties);
		destination.visualProperties = new HashMap<String, Object>(source.visualProperties);
		destination.properties = new HashMap<String, String>(source.properties);
	}
	
	/**
	 * Returns the list of cloned items in <code>source</code>.
	 * If a cloned item is selected its clone will be selected too (ie added to 
	 * <code>destinationSelectedItems</code>).
	 */
	@SuppressWarnings("unchecked")
	private static <T extends Selectable> List<T> cloneSelectableItems(List<T> source,
			List<Selectable> sourceSelectedItems, List<Selectable> destinationSelectedItems)
	{
		List<T> destination = new ArrayList<T>(source.size());
		for (T item : source)
		{
			T clone = (T) item.clone();
			destination.add(clone);
			if (sourceSelectedItems.contains(item))
			{
				destinationSelectedItems.add(clone);
			}
			else if (item instanceof HomeFurnitureGroup)
			{
				// Check if furniture in group is selected
				List<HomePieceOfFurniture> sourceFurnitureGroup = ((HomeFurnitureGroup) item).getAllFurniture();
				List<HomePieceOfFurniture> destinationFurnitureGroup = null;
				for (int i = 0, n = sourceFurnitureGroup.size(); i < n; i++)
				{
					HomePieceOfFurniture piece = sourceFurnitureGroup.get(i);
					if (sourceSelectedItems.contains(piece))
					{
						if (destinationFurnitureGroup == null)
						{
							destinationFurnitureGroup = ((HomeFurnitureGroup) clone).getAllFurniture();
						}
						destinationSelectedItems.add(destinationFurnitureGroup.get(i));
					}
				}
			}
		}
		return destination;
	}
	
	/**
	 * Returns a deep copy of home selectable <code>items</code>.
	 */
	public static List<Selectable> duplicate(List<? extends Selectable> items)
	{
		List<Selectable> list = new ArrayList<Selectable>();
		for (Selectable item : items)
		{
			if (!(item instanceof Wall) // Walls are copied further
					&& !(item instanceof Camera) // Cameras and compass can't be duplicated
					&& !(item instanceof Compass))
			{
				list.add(item.clone());
			}
		}
		// Add to list a clone of walls list with their walls at start and end point set
		list.addAll(Wall.clone(getWallsSubList(items)));
		return list;
	}
	
	/**
	 * Returns a sub list of <code>items</code> that contains only home furniture.
	 */
	public static List<HomePieceOfFurniture> getFurnitureSubList(List<? extends Selectable> items)
	{
		return getSubList(items, HomePieceOfFurniture.class);
	}
	
	/**
	 * Returns a sub list of <code>items</code> that contains only walls.
	 */
	public static List<Wall> getWallsSubList(List<? extends Selectable> items)
	{
		return getSubList(items, Wall.class);
	}
	
	/**
	 * Returns a sub list of <code>items</code> that contains only rooms.
	 */
	public static List<Room> getRoomsSubList(List<? extends Selectable> items)
	{
		return getSubList(items, Room.class);
	}
	
	/**
	 * Returns a sub list of <code>items</code> that contains only labels.
	 * @since 5.0
	 */
	public static List<Polyline> getPolylinesSubList(List<? extends Selectable> items)
	{
		return getSubList(items, Polyline.class);
	}
	
	/**
	 * Returns a sub list of <code>items</code> that contains only dimension lines.
	 */
	public static List<DimensionLine> getDimensionLinesSubList(List<? extends Selectable> items)
	{
		return getSubList(items, DimensionLine.class);
	}
	
	/**
	 * Returns a sub list of <code>items</code> that contains only labels.
	 */
	public static List<Label> getLabelsSubList(List<? extends Selectable> items)
	{
		return getSubList(items, Label.class);
	}
	
	/**
	 * Returns a sub list of <code>items</code> that contains only instances of <code>subListClass</code>.
	 * @since 2.2
	 */
	@SuppressWarnings("unchecked")
	public static <T> List<T> getSubList(List<? extends Selectable> items, Class<T> subListClass)
	{
		List<T> subList = new ArrayList<T>();
		for (Selectable item : items)
		{
			if (subListClass.isInstance(item))
			{
				subList.add((T) item);
			}
		}
		return subList;
	}
}
